# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import hashlib
import hmac
import json
import logging
import time
import urllib.parse

import requests.auth

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.binance.enum_types import (
    BinanceOrderSide,
    BinanceOrderType,
    BinanceNewOrderRespType,
    BinanceTimeInForce,
)

BINANCE_API_URL = 'api.binance.com'


def is_binance_stream_url(url):
  return '/api/v1/userDataStream' in url or '/fapi/v1/listenKey' in url


class BinanceAuth(requests.auth.AuthBase):
  def __init__(self, key_file):
    self._key = AuthKey.from_file(key_file)

  def _gen_signature(self, data: bytes):
    signature = hmac.new(self._key.secret_key_bytes, data, digestmod=hashlib.sha256)
    return signature.hexdigest()

  def __call__(self, req: requests.PreparedRequest):
    if not is_binance_stream_url(req.url):
      query = urllib.parse.urlparse(req.url).query
      signature = self._gen_signature(query.encode())
      req.url = '%s&signature=%s' % (req.url, signature)
    req.headers.update({'X-MBX-APIKEY': self._key.access_key})
    return req


class BinanceSyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, timeout=10):
    self._timeout = timeout
    self._auth = BinanceAuth(key_file)
    self._api_host = "https://" + (api_host or BINANCE_API_URL)
    self.rate_limit = RateLimiter(20, 1)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, method, path, params):
    assert method in ('GET', 'PUT', 'POST', 'DELETE'), method
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.request(method,
                                  url,
                                  params=params,
                                  auth=self._auth,
                                  timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException as e:
      self._logger.exception('RequestException: %s', e)
      raise
    except json.JSONDecodeError as e:
      self._logger.exception('JsonDecodeException: %s\n %s', response.content, e)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


def binance_req_timestamp():
  return int(time.time() * 1000)


class BinanceAsyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, timeout=10, max_connections=4):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = BinanceAuth(key_file)
    self._api_host = "https://" + (api_host or BINANCE_API_URL)

  def query(self, method, path, params=None):
    assert method in ('GET', 'PUT', 'POST', 'DELETE'), method
    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(method=method,
                                     url=url,
                                     params=params,
                                     auth=self._auth,
                                     timeout=self._timeout)
    return future


class BinanceNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      timeout=10,
      max_connections=8,
      recv_window=None,  # default to 5000
      use_async=False,
  ):
    if use_async:
      self._auth_queryer = BinanceAsyncQueryer(key_file,
                                               api_host=api_host,
                                               timeout=timeout,
                                               max_connections=max_connections)
    else:
      self._auth_queryer = BinanceSyncQueryer(key_file, api_host=api_host, timeout=timeout)

    self._recv_window = recv_window

  def _query(self, method, path, params=None):
    return self._auth_queryer.query(method, path, params=params)

  def place_order(
      self,
      *,
      symbol,
      price,
      quantity,
      order_side: BinanceOrderSide,
      order_type: BinanceOrderType,
      time_in_force=None,
      new_client_order_id=None,
      stop_price=None,
      iceberg_qty=None,
      new_order_resp_type=None,
  ):
    method = 'POST'
    path = '/api/v3/order'

    assert isinstance(order_side, BinanceOrderSide), type(order_side)
    assert isinstance(order_type, BinanceOrderType), type(order_type)

    if new_order_resp_type is not None:
      assert isinstance(new_order_resp_type, BinanceNewOrderRespType), type(new_order_resp_type)
      new_order_resp_type = new_order_resp_type.name

    if time_in_force is not None:
      assert isinstance(time_in_force, BinanceTimeInForce), type(time_in_force)
      time_in_force = time_in_force.name
    else:
      if order_type == BinanceOrderType.LIMIT:
        time_in_force = 'GTC'
      elif order_type == BinanceOrderType.LIMIT_MAKER:
        pass
      else:
        raise ValueError("not supported order_type: %s" % order_type)

    params = {
        'symbol': symbol,
        'side': order_side.name,
        'type': order_type.name,
        'timeInForce': time_in_force,
        'quantity': quantity,
        'price': price,
        'newClientOrderId': new_client_order_id,
        'stopPrice': stop_price,
        'icebergQty': iceberg_qty,
        'newOrderRespType': new_order_resp_type,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_order(
      self,
      symbol,
      order_id=None,
      orig_client_order_id=None,
  ):
    method = 'GET'
    path = '/api/v3/order'
    params = {
        'symbol': symbol,
        'orderId': order_id,
        'origClientOrderId': orig_client_order_id,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def cancel_order(
      self,
      symbol,
      order_id=None,
      orig_client_order_id=None,
      new_client_order_id=None,
  ):
    method = 'DELETE'
    path = '/api/v3/order'

    # Either orderId or origClientOrderId must be sent.
    assert order_id or orig_client_order_id, (order_id, orig_client_order_id)

    params = {
        'symbol': symbol,
        'orderId': order_id,
        'origClientOrderId': orig_client_order_id,
        'newClientOrderId': new_client_order_id,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_open_orders(self, symbol):
    method = 'GET'
    path = '/api/v3/openOrders'
    params = {
        'symbol': symbol,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_all_orders(
      self,
      symbol,
      order_id=None,
      start_time=None,
      end_time=None,
      limit=None,
  ):
    method = 'GET'
    path = '/api/v3/allOrders'

    if limit is not None:
      assert 0 < limit <= 1000, limit

    params = {
        'symbol': symbol,
        'orderId': order_id,
        'startTime': start_time,
        'endTime': end_time,
        'limit': limit,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_account(self):
    method = 'GET'
    path = '/api/v3/account'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_margin_account(self):
    method = 'GET'
    path = '/sapi/v1/margin/account'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_margin_isolated_account(self):
    method = 'GET'
    path = '/sapi/v1/margin/isolated/account'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_trade_list(
      self,
      symbol,
      start_time=None,
      end_time=None,
      from_id=None,
      limit=None,
  ):
    method = 'GET'
    path = '/api/v3/myTrades'

    if limit is not None:
      assert 0 < limit <= 1000, limit

    params = {
        'symbol': symbol,
        'startTime': start_time,
        'endTime': end_time,
        'fromId': from_id,
        'limit': limit,
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def start_data_stream(self):
    method = 'POST'
    path = '/api/v1/userDataStream'
    update = self._query(method, path)
    return update

  def keep_alive_data_stream(self, listen_key):
    method = 'PUT'
    path = '/api/v1/userDataStream'
    params = {'listenKey': listen_key}
    update = self._query(method, path, params)
    return update

  def close_data_stream(self, listen_key):
    method = 'DELETE'
    path = '/api/v1/userDataStream'
    params = {'listenKey': listen_key}
    update = self._query(method, path, params)
    return update

  def query_account_status(self):
    method = 'GET'
    path = '/wapi/v3/accountStatus.html'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_system_status(self):
    method = 'GET'
    path = '/wapi/v3/systemStatus.html'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_trade_fee(self):
    method = 'GET'
    path = '/wapi/v3/tradeFee.html'
    params = {
        'recvWindow': self._recv_window,
        'timestamp': binance_req_timestamp(),
    }
    update = self._query(method, path, params)
    return update

  def query_withdraw_history(self, currency):
    method = 'GET'
    path = '/sapi/v1/capital/withdraw/history'
    params = {'coin': currency, 'timestamp':binance_req_timestamp()}
    update = self._query(method, path, params)
    return update

  def query_deposit_history(self, currency):
    method = 'GET'
    path = '/sapi/v1/capital/deposit/hisrec'
    params = {'coin': currency, 'timestamp':binance_req_timestamp()}
    update = self._query(method, path, params)
    return update


if __name__ == "__main__":
  client = BinanceNativePrivateClient(
      key_file='../../coin_key/view/prestoinv0/binance/view_key.json')
  update = client.query_margin_account()
  print(update.msg)
  update = client.query_margin_isolated_account()
  print(update.msg)
